/*
 * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *      http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package me.ahoo.wow.test.saga.stateless

import me.ahoo.test.asserts.assert
import me.ahoo.wow.api.command.CommandMessage
import me.ahoo.wow.saga.stateless.CommandStream
import kotlin.reflect.KClass

/**
 * Interface for expecting results in stateless saga testing.
 *
 * This interface provides a fluent API for asserting expectations on the results
 * of stateless saga processing, including commands, errors, and command streams.
 *
 * @param T The type of the result being expected.
 * @param SE The self-referential type for method chaining.
 */
interface StatelessSagaExpecter<T : Any, SE : StatelessSagaExpecter<T, SE>> {
    /**
     * Sets expectations on the expected result using a lambda.
     *
     * @param expected A lambda function that configures expectations on an [ExpectedResult] instance.
     * @return The expecter instance for method chaining.
     */
    fun expect(expected: ExpectedResult<T>.() -> Unit): SE

    /**
     * Sets expectations on the command stream, ensuring no error occurred first.
     *
     * This method first verifies that no error occurred, then applies the provided
     * expectations to the command stream.
     *
     * @param expected A lambda function that configures expectations on the [CommandStream].
     * @return The expecter instance for method chaining.
     */
    fun expectCommandStream(expected: CommandStream.() -> Unit): SE =
        expectNoError().expect {
            commandStream.assert().describedAs { "Expect the command stream is not null." }.isNotNull()
            expected(commandStream!!)
        }

    /**
     * Sets expectations on a command iterator created from the command stream.
     *
     * This method creates a [CommandIterator] from the command stream's iterator
     * and applies the provided expectations to it.
     *
     * @param expected A lambda function that configures expectations on the [CommandIterator].
     * @return The expecter instance for method chaining.
     */
    fun expectCommandIterator(expected: CommandIterator.() -> Unit): SE =
        expectCommandStream {
            expected(CommandIterator(iterator()))
        }

    /**
     * Expects that no commands were generated by the saga.
     *
     * This is equivalent to calling [expectCommandCount] with 0.
     *
     * @return The expecter instance for method chaining.
     * @see expectCommandCount
     */
    fun expectNoCommand(): SE = expectCommandCount(0)

    /**
     * Sets expectations on the first command in the command stream.
     *
     * This method expects at least one command to be present and applies
     * the expectations to the first command message.
     *
     * @param C The type of the command body.
     * @param expected A lambda function that configures expectations on the first [CommandMessage].
     * @return The expecter instance for method chaining.
     */
    fun <C : Any> expectCommand(expected: CommandMessage<C>.() -> Unit): SE =
        expectCommandStream {
            assert()
                .describedAs { "Expect the command stream size to be greater than 1." }
                .hasSizeGreaterThanOrEqualTo(1)
            @Suppress("UNCHECKED_CAST")
            expected(first() as CommandMessage<C>)
        }

    /**
     * Sets expectations on the body of the first command.
     *
     * This method applies expectations directly to the body of the first command message.
     *
     * @param C The type of the command body.
     * @param expected A lambda function that configures expectations on the command body.
     * @return The expecter instance for method chaining.
     */
    fun <C : Any> expectCommandBody(expected: C.() -> Unit): SE =
        expectCommand {
            expected(body)
        }

    /**
     * Expects a specific number of commands in the command stream.
     *
     * @param expected The expected number of commands.
     * @return The expecter instance for method chaining.
     */
    fun expectCommandCount(expected: Int): SE =
        expectCommandStream {
            assert().describedAs { "Expect the command stream size." }.hasSize(expected)
        }

    /**
     * Expects commands of specific types in order.
     *
     * This method verifies that the command stream contains exactly the specified
     * number of commands and that each command's body is an instance of the
     * corresponding expected type.
     *
     * @param expected Variable number of [KClass] representing the expected command body types in order.
     * @return The expecter instance for method chaining.
     */
    fun expectCommandType(vararg expected: KClass<*>): SE =
        expectCommandCount(expected.size).expectCommandStream {
            val itr = iterator()
            for (eventType in expected) {
                itr.next()
                    .body
                    .assert()
                    .isInstanceOf(eventType.java)
            }
        }

    /**
     * Expects commands of specific types in order (Java Class version).
     *
     * This is a convenience overload that accepts Java [Class] instances
     * and converts them to Kotlin [KClass] for type checking.
     *
     * @param expected Variable number of [Class] representing the expected command body types in order.
     * @return The expecter instance for method chaining.
     */
    fun expectCommandType(vararg expected: Class<*>): SE = expectCommandType(*expected.map { it.kotlin }.toTypedArray())

    /**
     * Expects that no error occurred during saga processing.
     *
     * @return The expecter instance for method chaining.
     */
    fun expectNoError(): SE =
        expect {
            error.assert().describedAs { "Expect no error." }.isNull()
        }

    /**
     * Expects that an error occurred during saga processing.
     *
     * @return The expecter instance for method chaining.
     */
    fun expectError(): SE =
        expect {
            error.assert().describedAs { "Expect an error." }.isNotNull()
        }

    /**
     * Expects a specific error with custom assertions.
     *
     * This method first ensures an error occurred, then applies the provided
     * expectations to the error instance.
     *
     * @param E The type of the expected error.
     * @param expected A lambda function that configures expectations on the error.
     * @return The expecter instance for method chaining.
     */
    fun <E : Throwable> expectError(expected: E.() -> Unit): SE =
        expectError().expect {
            @Suppress("UNCHECKED_CAST")
            expected(error as E)
        }

    /**
     * Expects an error of a specific type.
     *
     * @param E The type of the expected error.
     * @param expected The [KClass] of the expected error type.
     * @return The expecter instance for method chaining.
     */
    fun <E : Throwable> expectErrorType(expected: KClass<E>): SE =
        expectError<E> {
            assert().isInstanceOf(expected.java)
        }

    /**
     * Expects an error of a specific type (Java Class version).
     *
     * This is a convenience overload that accepts a Java [Class] instance.
     *
     * @param E The type of the expected error.
     * @param expected The [Class] of the expected error type.
     * @return The expecter instance for method chaining.
     */
    fun <E : Throwable> expectErrorType(expected: Class<E>): SE = expectErrorType(expected.kotlin)
}
